Skip to content

fix(core): do not reuse an HTTP/1 connection after a 204 that frames a body - #1112

Open
CorsenAI wants to merge 1 commit into
cloudflare:mainfrom
CorsenAI:fix/h1-no-content-reuse
Open

CorsenAI wants to merge 1 commit into
cloudflare:mainfrom
CorsenAI:fix/h1-no-content-reuse

Conversation

@CorsenAI

@CorsenAI CorsenAI commented Nov 6, 2026

Copy link
Copy Markdown

Summary

Refuse to reuse an HTTP/1 upstream connection after a 204 No Content response that carries Transfer-Encoding or a non-zero Content-Length.

Why

A 204 is terminated by the first empty line after its header fields regardless of the fields present (RFC 9112 §6.3), and a server MUST NOT send Transfer-Encoding or Content-Length with it (RFC 9112 §6.1, RFC 9110 §8.6). The client correctly reads no body after a 204, and it already refuses to reuse a connection when extra bytes are found in the read buffer or on the idle connection in the pool.

Those checks can only see bytes that have already arrived. If a non-conformant server sends the body it announced after the connection has been handed to the next request, that request parses those bytes as its response, and every later response on the connection is attributed to the wrong request.

On a 204 the two header fields are a free, precise signal of that server behaviour, so this change treats them as "the server may still send a body" and disables keep-alive for the session in respect_keepalive(). Conformant servers are unaffected; a 204 with Content-Length: 0, which is common in practice and announces no body, keeps the connection reusable.

Responses to HEAD requests and 304 responses may legitimately carry these fields as metadata, so they are intentionally left unchanged.

Tests

  • protocols::http::v1::client: a 204 with Content-Length: 128, Content-Length: 1, Transfer-Encoding: chunked, or Transfer-Encoding: chunked plus Content-Length: 0 is still read as bodyless, disables keep-alive even with Connection: keep-alive, and reuse() returns None. A 204 with no framing fields or with Content-Length: 0 stays reusable.
  • connectors::http::v1: against a loopback server that sends only the header section of a 204 with Content-Length: 128 or Transfer-Encoding: chunked, the released session is not returned to the pool. With Content-Length: 0 it is.

The six cases that frame a body fail on main and pass with this change. cargo fmt --check, cargo clippy -p pingora-core --all-targets -- --deny=warnings, and the HTTP/1 protocol and connector test modules pass locally.

…a body

A 204 response ends at the first empty line after its header fields
regardless of the fields present (RFC 9112 section 6.3), and a server
must not send Transfer-Encoding or Content-Length with it (RFC 9112
section 6.1, RFC 9110 section 8.6). The client already reads no body
after a 204, and it refuses to reuse the connection when extra bytes are
found in the read buffer or on the idle connection in the pool.

Body bytes that a non-conformant server sends after the connection has
been handed to the next request escape those checks: they are parsed as
the response to that request, and every later response on the
connection is attributed to the wrong request.

Treat Transfer-Encoding or a non-zero Content-Length on a 204 as the
signal that the server may still send a body, and disable keep-alive for
that session in respect_keepalive(). Conformant servers are unaffected,
and a 204 with Content-Length: 0 keeps the connection reusable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant